Skip to content

RM: configurable CEM endpoint, stable structural IDs, richer dashboard#16

Open
martijnversluis wants to merge 3 commits into
mainfrom
rm-configurable-cem
Open

RM: configurable CEM endpoint, stable structural IDs, richer dashboard#16
martijnversluis wants to merge 3 commits into
mainfrom
rm-configurable-cem

Conversation

@martijnversluis

Copy link
Copy Markdown
Collaborator

Three focused fixes to the rm-mqtt-ws-bridge reference implementation. Each is a separate commit so it can be reviewed on its own.

1. Route the RM to a configurable CEM endpoint

Today the RM in the three-panel demo is hard-wired to the bundled stand-in in rm-mqtt-ws-bridge/cem/. This makes it awkward to interoperate with cem-reference-1 or any external CEM without editing the compose file.

The change turns CEM_BASE_URL into a proper knob:

  • External CEMCEM_BASE_URL=wss://your-cem.example/ws ./demo.sh routes the RM there and skips the bundled stand-in.
  • Reference CEM — with no CEM_BASE_URL, demo.sh probes cem-reference-1 on localhost:8003/health and, when it answers, wires the RM to ws://host.docker.internal:8003/ws. Cross-container-to-host works through the newly added extra_hosts: host.docker.internal:host-gateway on the RM service.
  • Simulation — otherwise it falls back to the bundled cem stand-in, now guarded by a bundled-cem compose profile so it only starts in this mode. depends_on: cem on both rm and demo uses required: false so the graph stays valid without it.
  • MOSQUITTO_HOST_PORT and RESOURCE_ID were also promoted to passthroughs so the defaults can be overridden without editing files.

The three-panel demo screen (demo/index.html) was hard-coding ws://localhost:9000/s2 and always framing the CEM stand-in iframe. It now fetches /traffic from the RM at page-load (and every 2 s), fills the address bar with the live cem_endpoint, and, when that URL points outside the compose network, replaces the CEM iframe with an "S2 CEM external" info tile that shows the endpoint and the link state. When the bundled stand-in is active the iframe is restored, so the original click-through walkthrough still works. TrafficController sends Access-Control-Allow-Origin: * so the demo panel can read /traffic cross-origin.

2. Keep FRBC system description IDs stable across reconnects

FRBCSystemDescriptionFactory generated the actuator id, both operation mode ids, and both transition ids with SecureRandom.uuid. Every reconnect (or RM restart) produced a fresh set, so any FRBC.Instruction a CEM had already persisted referenced actuator/operation_mode ids that no longer existed in the new system description. On the wepositive CEM this shows up as a red warning triangle next to those fields on stored instructions.

The structural ids are now derived deterministically with Digest::UUID.uuid_v5 from a fixed namespace and <resource.s2_identifier>/<role>. Same resource + same role → same UUID, across reconnects and restarts, with no persistent state to maintain. Only message_id stays random per emission, as the S2 spec requires.

3. Fold reception statuses and raw bodies into the RM dashboard feed

The RM dashboard feed had four rough edges:

  • ReceptionStatus was rendered as its own row, doubling the feed length and hiding which message it acked.
  • Handshake / HandshakeResponse / SelectControlType / ResourceManagerDetails / FRBC.SystemDescription rows had no direction indicator, so RM→CEM and CEM→RM looked identical.
  • The raw S2 JSON was not viewable; the MQTT panel already offered "> raw message".
  • The InstructionStatusUpdate that the RM sends back after processing an FRBC.Instruction was silently dropped by the render filter.

The change:

  • TrafficLog now stores message_id, subject_message_id, status, and the parsed body per entry. S2MessageLogger and MQTTMessageLogger extract those fields from the payload.
  • ReceptionStatus is no longer its own row. Every S2 message row carries a small badge: green ✓ (OK), red ! (any other status), grey … (waiting). Clicking the badge opens the raw ReceptionStatus JSON inline in a green-tinted block.
  • Clicking anywhere else on a message row toggles the raw JSON body of that message, matching the "> raw message" affordance of the MQTT panel. Open/closed state survives the 1.2 s poll cycle via a stable id hashed from peer/direction/timestamp/message_id/subject_message_id.
  • Setup rows now carry a direction arrow so RM→CEM (, toward the CEM column) and CEM→RM () are distinguishable at a glance, matching the existing arrow convention on report and command rows.
  • The FWD map gained instruction_update → InstructionStatusUpdate, so the reverse-path acknowledgement now surfaces on its own row (with the CEM's ack badge) instead of being filtered out.

The RM defaults to the bundled demo CEM stand-in inside rm-mqtt-ws-bridge/cem/.
This change lets an operator point the RM at any CEM without editing compose
files:

* CEM_BASE_URL is now a docker-compose passthrough. demo.sh reads it and, if
  unset, probes cem-reference-1 on localhost:8003 and points the RM at
  ws://host.docker.internal:8003/ws when it answers. Only if there is no
  CEM_BASE_URL and no reference CEM running does it fall back to the bundled
  stand-in, which now sits under a "bundled-cem" compose profile so it stays
  offline in the other two modes.
* MOSQUITTO_HOST_PORT and RESOURCE_ID were also promoted to passthroughs so
  they can be overridden when the defaults clash with something on the host.
* The three-panel demo screen no longer hard-codes ws://localhost:9000/s2 or
  the CEM iframe. It fetches /traffic from the RM on load (and every 2s) and,
  when CEM_BASE_URL points outside the compose network, replaces the CEM
  iframe with an "S2 CEM external" tile that shows the live endpoint and link
  state. When the bundled stand-in is active the iframe is restored, so the
  original click-through demo still works.
* TrafficController now sets Access-Control-Allow-Origin so the demo panel
  can read /traffic from a different origin.
FRBCSystemDescriptionFactory used SecureRandom.uuid for the actuator, both
operation modes, and the transitions. Every reconnect (or RM restart) produced
a fresh set, so any earlier FRBC.Instruction persisted on the CEM referenced
IDs that no longer existed in the current system description. The wepositive
CEM surfaces that as a warning triangle next to the actuator_id and
operation_mode fields on stored instructions.

The structural IDs are now derived deterministically via UUIDv5, namespaced
per role and per resource:

    Digest::UUID.uuid_v5(NAMESPACE, "<resource.s2_identifier>/<role>")

Same resource + same role -> same UUID, across reconnects and restarts, with
no persistent state to maintain. Only message_id stays random per emission,
as the S2 spec requires.
The RM dashboard feed treated ReceptionStatus as an ordinary row, gave S2
setup messages no direction indicator, hid the raw payload of S2 messages
entirely, and dropped the RM->CEM InstructionStatusUpdate that follows the
reverse path.

* TrafficLog now stores the parsed body plus message_id / subject_message_id
  / status for each entry. S2MessageLogger and MQTTMessageLogger extract
  those fields from the payload and pass them through.
* ReceptionStatus is no longer its own row. Every S2 message row shows a
  small badge next to its message type: green tick for status=OK, red !
  for any other status, grey ... while no ack has arrived yet. The badge
  is a toggle: clicking it opens the raw ReceptionStatus JSON inline.
* Every S2 and MQTT message row is clickable and expands to reveal its raw
  JSON body, matching the "> raw message" affordance the MQTT panel already
  provides. Open/closed state is preserved across the 1.2s poll cycle via a
  stable id hashed from peer/direction/timestamp/message_id/subject_message_id.
* Setup rows (Handshake, HandshakeResponse, ResourceManagerDetails,
  SelectControlType, FRBC.SystemDescription) now carry a direction arrow so
  RM->CEM and CEM->RM are distinguishable at a glance.
* The FWD map now includes instruction_update -> InstructionStatusUpdate,
  so the RM->CEM acknowledgement of an FRBC.Instruction shows up on its own
  row (with the CEM's ack badge) instead of being silently dropped.
@jorritn

jorritn commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Cool. Really nice improvements @martijnversluis

One thing worth mentioning: it is not wrong to use reuse Ids in the FRBC system description but from the S2 pov also not required: a CEM should not require the Ids to be stable over multiple sessions. Every session is fully self contained and state is not supposed to be shared over multiple session. Nevertheless, for debugging purposes and system insights it can be helpful to use the same Ids.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants